Bound every action-log read path - #298
Merged
Merged
Conversation
ndisidore
force-pushed
the
chore/scale-action-logs
branch
from
August 21, 2026 19:51
7a6dcf3 to
eef947d
Compare
ndisidore
force-pushed
the
chore/scale-action-logs
branch
from
August 21, 2026 20:12
03c86bd to
eef947d
Compare
kentonv
reviewed
Aug 24, 2026
ndisidore
force-pushed
the
chore/scale-action-logs
branch
from
August 24, 2026 20:32
eef947d to
dee1f52
Compare
Preview:
|
kentonv
reviewed
Aug 25, 2026
ndisidore
force-pushed
the
chore/scale-action-logs
branch
from
August 25, 2026 14:18
cc8359a to
cee048e
Compare
ndisidore
force-pushed
the
chore/scale-action-logs
branch
from
August 25, 2026 15:14
cee048e to
bfe5841
Compare
ndisidore
force-pushed
the
chore/scale-action-logs
branch
from
August 25, 2026 17:06
bfe5841 to
13726ee
Compare
kentonv
reviewed
Aug 25, 2026
Review round 3: replace the id-ordered resume sweep (still O(log) per resubscribe) with a byLastChanged unique index -- last state-change time, id-disambiguated for the frozen clock -- so startAfter replays only the records changed during the gap, inclusively (an exclusive bound loses same-instant siblings). Merge the two index migrations into one v2 -> 3 step covering all three action indexes, drop durationMs from its log (always zero in sync code), drop byHistoryFilter's redundant "all" bucket (the collection itself already serves it in id order), and trim comments per review.
ndisidore
added a commit
that referenced
this pull request
Aug 25, 2026
Drop the pending-set carryover: a settled store now parks just its last change time by workspace key, and the next linked store opens exactly as a cold one — subscribe, then page pending — passing the watermark as startAfter. The gap still replays as upserts through the subscription (which is what useActionHistory and ChatInterface rely on), the fresh pages re-snapshot the pending set, and the page loop stays the single settled signal. Buys back the seeding/skip-page dual path for the cost of re-paging a set PR #298 already made cheap.
kentonv
approved these changes
Aug 25, 2026
ndisidore
added a commit
that referenced
this pull request
Aug 25, 2026
Drop the pending-set carryover: a settled store now parks just its last change time by workspace key, and the next linked store opens exactly as a cold one — subscribe, then page pending — passing the watermark as startAfter. The gap still replays as upserts through the subscription (which is what useActionHistory and ChatInterface rely on), the fresh pages re-snapshot the pending set, and the page loop stays the single settled signal. Buys back the seeding/skip-page dual path for the cost of re-paging a set PR #298 already made cheap.
ndisidore
added a commit
that referenced
this pull request
Aug 25, 2026
Drop the pending-set carryover: a settled store now parks just its last change time by workspace key, and the next linked store opens exactly as a cold one — subscribe, then page pending — passing the watermark as startAfter. The gap still replays as upserts through the subscription (which is what useActionHistory and ChatInterface rely on), the fresh pages re-snapshot the pending set, and the page loop stays the single settled signal. Buys back the seeding/skip-page dual path for the cost of re-paging a set PR #298 already made cheap.
ndisidore
added a commit
that referenced
this pull request
Aug 25, 2026
* feat(frontend): resume action subscriptions with startAfter on reconnect A settled shared action store now parks its pending set and change-time watermark (max appliedAt ?? createdAt received) by workspace key when it closes. The next store linked to the same key seeds from that carryover and subscribes with startAfter, so the server replays only the gap as upserts instead of the store re-paging the whole pending set — the subscribe call resolving is the settled signal. Unsettled or errored sessions never seed a resume, and unlinked stubs keep cold-open behavior. useWorkspaceOpen links every minted Overseer stub to its workspace id. useActionHistory keeps its loaded window and cursor across a resumed swap (dropping in-flight old-stub pages), and ChatInterface's gap-refetch effect is now only the cold-open safety net, resolving its startAfter TODO. * refactor(frontend): carry only a resume watermark across reconnects Drop the pending-set carryover: a settled store now parks just its last change time by workspace key, and the next linked store opens exactly as a cold one — subscribe, then page pending — passing the watermark as startAfter. The gap still replays as upserts through the subscription (which is what useActionHistory and ChatInterface rely on), the fresh pages re-snapshot the pending set, and the page loop stays the single settled signal. Buys back the seeding/skip-page dual path for the cost of re-paging a set PR #298 already made cheap. * fix(frontend): recover from failed action log resume * fix(frontend): harden the resume watermark and repair channel Review fixes for the reconnect-resume stack: A store now parks its watermark only after a cleanly settled session: pages drained AND the subscribe call resolved (the server delivers the gap replay before resolving, so 'ready' alone can predate undelivered replay records) AND no entry listener threw on a delivery. ChatInterface carries cards a refetch run failed to repair (error, or cancelled mid-loop) to the next reconnect and retries them even when the resume replay covers everything else — restoring the repair channel the resume skip removed. Also: actionChangeTime() in workshop-shared now owns the appliedAt ?? createdAt formula shared by the server's byLastChanged index key, the client watermark, and Activity's display; a status-only useActionStatus() spares ChatInterface/useActionHistory a re-render per pending-set change; useActionHistory dedupes its session resets; the ChatInterface action test uses the shared harness root (fixing a leaked rAF queue) — plus regression tests for the three behavior changes. * refactor(frontend): drop the resume failure fallbacks The consumer-side recovery shell (useActionStatus, resumeFallbackRequired, unrepairedCardsRef, entryListenerFailed) defended a resumed subscription failing while the stub stays healthy — reachable only through a server-side bug, since transport failures swap the stub. The store's invariant already guarantees eventual healing without it: a failed session never parks a watermark, so the next stub swap replays its entire gap from the last good one. Until then the store shows status 'error', so nothing degrades silently. entryListenerFailed additionally protected nothing real: the refetch path applies updates through the same applyActionLogUpdateToCachedMessages the listener uses, so a record that throws in one path throws in both. Kept the subscription-resolved watermark gate — a page-only watermark is poison (a pending record's createdAt can exceed a missed resolution's appliedAt, hiding it from every future replay) — and documented on actionLogResumed why consumers may trust it without a failure path.
darjss
pushed a commit
to darjss/cloudflare-os-erxes
that referenced
this pull request
Aug 26, 2026
* feat(typed-storage): support ranged reads and index rebuilds * feat(shared+backend): add indexed paginated action history * fix(backend): bound action replay and auto-approval scans * feat(frontend): page action history and reconcile live actions * fix(backend): index the resume replay by last change time Review round 3: replace the id-ordered resume sweep (still O(log) per resubscribe) with a byLastChanged unique index -- last state-change time, id-disambiguated for the frozen clock -- so startAfter replays only the records changed during the gap, inclusively (an exclusive bound loses same-instant siblings). Merge the two index migrations into one v2 -> 3 step covering all three action indexes, drop durationMs from its log (always zero in sync code), drop byHistoryFilter's redundant "all" bucket (the collection itself already serves it in id order), and trim comments per review.
darjss
pushed a commit
to darjss/cloudflare-os-erxes
that referenced
this pull request
Aug 26, 2026
* feat(frontend): resume action subscriptions with startAfter on reconnect A settled shared action store now parks its pending set and change-time watermark (max appliedAt ?? createdAt received) by workspace key when it closes. The next store linked to the same key seeds from that carryover and subscribes with startAfter, so the server replays only the gap as upserts instead of the store re-paging the whole pending set — the subscribe call resolving is the settled signal. Unsettled or errored sessions never seed a resume, and unlinked stubs keep cold-open behavior. useWorkspaceOpen links every minted Overseer stub to its workspace id. useActionHistory keeps its loaded window and cursor across a resumed swap (dropping in-flight old-stub pages), and ChatInterface's gap-refetch effect is now only the cold-open safety net, resolving its startAfter TODO. * refactor(frontend): carry only a resume watermark across reconnects Drop the pending-set carryover: a settled store now parks just its last change time by workspace key, and the next linked store opens exactly as a cold one — subscribe, then page pending — passing the watermark as startAfter. The gap still replays as upserts through the subscription (which is what useActionHistory and ChatInterface rely on), the fresh pages re-snapshot the pending set, and the page loop stays the single settled signal. Buys back the seeding/skip-page dual path for the cost of re-paging a set PR cloudflare#298 already made cheap. * fix(frontend): recover from failed action log resume * fix(frontend): harden the resume watermark and repair channel Review fixes for the reconnect-resume stack: A store now parks its watermark only after a cleanly settled session: pages drained AND the subscribe call resolved (the server delivers the gap replay before resolving, so 'ready' alone can predate undelivered replay records) AND no entry listener threw on a delivery. ChatInterface carries cards a refetch run failed to repair (error, or cancelled mid-loop) to the next reconnect and retries them even when the resume replay covers everything else — restoring the repair channel the resume skip removed. Also: actionChangeTime() in workshop-shared now owns the appliedAt ?? createdAt formula shared by the server's byLastChanged index key, the client watermark, and Activity's display; a status-only useActionStatus() spares ChatInterface/useActionHistory a re-render per pending-set change; useActionHistory dedupes its session resets; the ChatInterface action test uses the shared harness root (fixing a leaked rAF queue) — plus regression tests for the three behavior changes. * refactor(frontend): drop the resume failure fallbacks The consumer-side recovery shell (useActionStatus, resumeFallbackRequired, unrepairedCardsRef, entryListenerFailed) defended a resumed subscription failing while the stub stays healthy — reachable only through a server-side bug, since transport failures swap the stub. The store's invariant already guarantees eventual healing without it: a failed session never parks a watermark, so the next stub swap replays its entire gap from the last good one. Until then the store shows status 'error', so nothing degrades silently. entryListenerFailed additionally protected nothing real: the refetch path applies updates through the same applyActionLogUpdateToCachedMessages the listener uses, so a record that throws in one path throws in both. Kept the subscription-resolved watermark gate — a page-only watermark is poison (a pending record's createdAt can exceed a missed resolution's appliedAt, hiding it from every future replay) — and documented on actionLogResumed why consumers may trust it without a failure path.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reading the action log today means reading all of it: opening a workspace replays every record ever written to each client, the Activity pane holds the whole log in React state, and the auto-approval drain materializes the full table per run. Long-lived workspaces pay for this on every open, and the cost only grows.
This branch bounds every read path and splits the protocol into query-for-state / subscribe-for-deltas:
subscribeToActionsdelivers live updates only. Clients fetch the current pending set throughlistActions({filter: "pending"})after initiating the subscribe; capnweb e-order makes the pair gapless. The unpaced replay push is gone; initial state flows over pull-paged, client-clocked reads.listActionsRPC pages history newest-first: resolved records under a raw-scan cap, so a log buried in resolved records returns short pages with a cursor instead of stalling the DO, and pending records off a new sparse pending-by-gatekeeper indexAutoApprovalDrainerreads its gatekeeper's pendings off the index instead of scanning the log.startAfterget a paced full replayTested with unit suites for the subscription, pagination, migration-backfill, and drain paths, an integration smoke over the paged RPCs, and hook tests for the new frontend state.